## Super()

Мы продолжаем изучение темы «наследование». 

Это занятие я начну с простой, но важной терминологии. 

Смотрите, если у нас имеется некий базовый класс Geom и мы создаем дочерний класс Line, в котором дополнительно прописан метод `draw()`, то это называется расширением (extended) базового класса:

```python
class Geom:
    name = 'Geom'
 
 
class Line(Geom):
    def draw(self):
        print("Рисование линии")
```

Как правило, дочерние создаются именно для расширения функциональности базовых классов. Однако, если в классе Geom также прописать метод draw():

```python
class Geom:
    name = 'Geom'
 
    def draw(self):
        print("Рисование примитива")
```

то теперь класс `Line` лишь переопределяет (overriding) поведение базового класса, не меняя его принцип функционирования.

Поэтому, когда говорят о расширении, то подразумевают добавление новых атрибутов в дочерних классах, а при переопределении (обычно методов) – изменение поведения уже существующего функционала.

Функция `super()` и делегирование
Давайте теперь пропишем инициализатор в базовом классе Geom (метод draw уберем):
```python
class Geom:
    name = 'Geom'
 
    def __init__(self):
        print("инициализатор Geom")
```

А ниже создадим экземпляр класса Line:

```python
l = Line()
```

После запуска программы увидим ожидаемое поведение – был автоматически вызван инициализатор базового класса. 

В действительности, здесь происходит следующая последовательность вызовов магических методов. 

Сначала вызывается `__call__()`, который, в свою очередь, последовательно вызывает метод `__new__()` для создания экземпляра класса, а затем, метод `__init__()` для его инициализации. 

Так вот, все эти методы вызываются из дочернего класса `Line`.

Если какой-либо из них не находится, то поиск продолжается в родительских классах в порядке иерархии наследования. 

Например, метод `__new__()` в данном случае будет взят из метакласса `type`, который неявно вызывается при создании классов (подробнее о метаклассах мы еще будем говорить). 

А вот метод `__init__()` мы прописали в классе `Geom`, поэтому будет вызван именно он. 

Причем, параметр self в этом методе будет ссылаться на созданный объект класса `Line`. 

Об этом мы с вами уже говорили и это следует помнить. 

Параметр `self` в методах класса всегда ссылается на объект, из которого метод был вызван.

Отлично, я думаю в целом схема вызова методов в момент создания экземпляров классов, понятна. 

И в соответствии с ней, если мы определим инициализатор в классе `Line`, то именно он и должен вызваться. 


Давайте это сделаем:

```python
class Line(Geom):
    def __init__(self):
        print("инициализатор Line")
 
    def draw(self):
        print("Рисование линии")
```

Запустим программу и теперь видим, что действительно, вызывается именно метод `__init__` класса `Line`. 

Я перепишу его со следующими параметрами:
```python
    def __init__(self, x1, y1, x2, y2):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
```

Это координаты начала и конца линии на плоскости. 

Соответственно, при создании объектов этого класса, мы теперь должны передавать аргументы:

```python
l = Line(0, 0, 10, 20)
```

Все работает и никаких проблем у нас нет. 

Но, давайте теперь добавим еще один класс Rect для прямоугольников:

```python
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill=None):
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
        self.fill = fill
 
    def draw(self):
        print("Рисование прямоугольника")
```

Смотрите, у нас получилось дублирование кода.

Это очень нехорошо. 

Но мы знаем, как это можно поправить. 

Давайте общее этих методов вынесем в базовый класс Geom:

```python
class Geom:
    name = 'Geom'
 
    def __init__(self, x1, y1, x2, y2):
        print(f"инициализатор Geom для {self.__class__}")
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
```

В дочернем классе Line уберем инициализатор, т.к. он полностью повторяется в Geom, а класс Rect запишем в виде:

```python
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill=None):
        print("инициализатор Rect")
        self.fill = fill
 
    def draw(self):
        print("Рисование прямоугольника")
```
Ниже создадим экземпляры обоих классов:

```python
l = Line(0, 0, 10, 20)
r = Rect(1, 2, 3, 4)
```

После запуска увидим следующее:

```
инициализатор Geom для <class '__main__.Line'>
инициализатор Rect
```
О чем это говорит? 

Для класса `Line` был вызван инициализатор в базовом классе Geom, а для класса `Rect` не вызывался – только инициализатор самого класса. 

И это логично, так как метод `__init__()` был найден в `Rect` и дальше цепочка поиска не продолжалась.

 Но нам же нужно при создании примитивов также вызывать инициализатор и базового класса Geom. 
 
 Как это сделать? 
 
 Конечно, мы могли бы явно указать имя базового класса Geom и вызвать через него магический метод `__init__()` в инициализаторе класса Rect:

```python
    def __init__(self, x1, y1, x2, y2, fill=None):
        Geom.__init__(self, x1, y1, x2, y2)
        print("инициализатор Rect")
        self.fill = fill
```
Но явно указывать имена базовых классов не лучшая практика, так как имена и иерархия наследования могут меняться.

Поэтому в Python для обращения к базовому классу используется специальная функция super():

```python
    def __init__(self, x1, y1, x2, y2, fill=None):
        super().__init__(x1, y1, x2, y2)
        print("инициализатор Rect")
        self.fill = fill
```

Она возвращает ссылку на, так называемый, объект-посредник, через который происходит вызов методов базового класса.

Теперь, при запуске программы мы видим, что был вызван инициализатор сначала класса Geom, а затем, для `Rect`. 

Такое обращение к переопределенным методам базового класса с помощью функции `super()` называется делегированием. 

То есть, мы делегировали вызов инициализатора класса Geom, чтобы он создал в нашем объекте локальные свойства с координатами углов прямоугольника. 

Причем, вызов метода `__init__()` базового класса лучше делать в первой же строчке, чтобы он случайно не переопределял какие-либо локальные свойство в дочернем классе.

 Например, если в базовом `__init__()` дополнительно прописать:

```python
    def __init__(self, x1, y1, x2, y2):
        print(f"инициализатор Geom для {self.__class__}")
        self.x1 = x1
        self.y1 = y1
        self.x2 = x2
        self.y2 = y2
        self.fill = 0
```


А в дочернем его вызвать в последнюю очередь:

```python
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill=None):
        print("инициализатор Rect")
        self.fill = fill
        super().__init__(x1, y1, x2, y2)
```

то, очевидно, свойство `fill` будет неявно переопределено при вызове `__init__()` базового класса:

```python
print(r.__dict__)
```

Подобные причины и рекомендуют делать вызов инициализатора базового класса в первой же строчке:

```python
class Rect(Geom):
    def __init__(self, x1, y1, x2, y2, fill=None):
        super().__init__(x1, y1, x2, y2)
        print("инициализатор Rect")
        self.fill = fill
```

Теперь у нас нет проблем с определением локального свойства `fill`.

Надеюсь, из этого вы узнали о понятиях расширения классов и переопределения их методов, разобрались с работой функции `super()`, а также делегированием методов, то есть их вызовом из базового класса через функцию `super()`.